package edu.uis.project.frankszoo;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Scanner;

import edu.uis.project.frankszoo.model.card.Card;
import edu.uis.project.frankszoo.model.deck.DeckBuilder;
import edu.uis.project.frankszoo.model.deck.PlayerDeck;
import edu.uis.project.frankszoo.model.event.GameEndedEvent;
import edu.uis.project.frankszoo.model.event.GameEngineEventListener;
import edu.uis.project.frankszoo.model.event.GameStartedEvent;
import edu.uis.project.frankszoo.model.event.HandEndedEvent;
import edu.uis.project.frankszoo.model.event.HandStartedEvent;
import edu.uis.project.frankszoo.model.event.PlayerOutOfCardsEvent;
import edu.uis.project.frankszoo.model.event.RoundEndedEvent;
import edu.uis.project.frankszoo.model.event.RoundStartedEvent;
import edu.uis.project.frankszoo.model.game.ActivePlayer;
import edu.uis.project.frankszoo.model.game.GameHand;
import edu.uis.project.frankszoo.model.game.GamePlayer;
import edu.uis.project.frankszoo.model.game.GameRound;
import edu.uis.project.frankszoo.model.score.GameScoreBoard;
import edu.uis.project.frankszoo.model.score.PlayerScore;

/**
 * This is the primary class that serves as a facade for all the operations of the Game. It contains most of the logic of the game.
 * 
 */
public class GameEngine implements GameConstants {

	private List<GameEngineEventListener> eventlisteners; // List containing listeners implementing the GameEngineEventListener interface
	private List<GamePlayer> players; // List of players in the game
	private List<GameHand> gameHands; // List of Hands played in the game
	private DeckBuilder deckBuilder; // Utility class which builds the deck
	private GameScoreBoard scoreBoard; // Scoreboard to keep track of player scores

	/**
	 * This is the default constructor for the GameEngine
	 */
	public GameEngine() {
		eventlisteners = new ArrayList<GameEngineEventListener>();
		players = new ArrayList<GamePlayer>();
		gameHands = new ArrayList<GameHand>();
		deckBuilder = new DeckBuilder();
		scoreBoard = new GameScoreBoard();
		addGameEngineEventListener(scoreBoard);
	}

	/**
	 * This is the public method, which triggers the entire game play. It inialized the gameplayers and starts the hand. The loop for hand continues
	 * until there are atleast two players with scores greater than or equal to 19
	 * 
	 * @throws Exception
	 */
	public void playGame() throws Exception {

		buildGamePlayers();
		fireGameStartedEvent(players);

		int handNo = 0;

		// Loop until atleast two players with scores greater than or equal to 19
		while (scoreBoard.continueGame()) {
			GameHand gameHand = new GameHand(++handNo);
			GamePlayer dealer = pickDealer(gameHand);
			List<ActivePlayer> activePlayers = buildActivePlayers(buildPlayerDecks(deckBuilder, dealer));
			gameHand.setDealerID(dealer.getPlayerID());
			gameHand.setActivePlayers(activePlayers);
			gameHands.add(gameHand);
			printActivePlayerDecks(gameHand);
			fireGameHandStartedEvent(handNo, dealer.getPlayerID());
			// Start the Hand
			playHand(gameHand);
			fireGameHandEndedEvent(handNo, gameHand.getEndingPlayerID(), gameHand.getActivePlayers());
		}
		fireGameEndedEvent();
	}

	/**
	 * This method runs most of the show. It encapsulates the game play within a hand. It creates rounds and loops until only one player is left with
	 * cards
	 * 
	 * @param gameHand
	 *            Instance of GameHand
	 * @throws Exception
	 */
	private void playHand(GameHand gameHand) throws Exception {
		int roundNo = 0;
		String roundWinnerID = null;
		int roundWinnerIndex = -1;

		// Play this hand until there is only one player left
		while (continueHand(gameHand)) {
			int startingPlayerIndex = getStartingPlayerIndex(gameHand, roundWinnerID, roundWinnerIndex);
			String startingPlayerID = getPlayerIDByIndex(gameHand, startingPlayerIndex);

			GameRound gameRound = new GameRound(++roundNo, startingPlayerID);
			gameHand.getGameRounds().add(gameRound);
			fireGameRoundStartedEvent(gameHand.getHandNo(), roundNo, startingPlayerID, gameHand.getActivePlayers());

			int currentPlayerIndex = startingPlayerIndex;
			int previousPlayerIndex = currentPlayerIndex;
			int activeCardSetPlayerIndex = -1;
			boolean isFirst = true;
			ActivePlayer currentPlayer = null;

			boolean continueRound = true;

			// Continue Round until Player OutRanks others.
			while (continueRound) {
				currentPlayer = gameHand.getActivePlayers().get(currentPlayerIndex);

				if (gameHand.getGameHandTable().getActiveSetOwner() != null) {
					activeCardSetPlayerIndex = getActiveCardSetPlayerIndex(gameHand);
				}

				if ((currentPlayerIndex != previousPlayerIndex) && (currentPlayerIndex == activeCardSetPlayerIndex)) {
					//
					System.out.println("\t\t" + currentPlayer.getPlayerName() + " outranked others and won the round");
					// Cleanup the gameTable and put the cards into trick of cards
					currentPlayer.getPlayerDeck().addToTrickOfCards(gameHand.getGameHandTable().getActiveCardSet());
					currentPlayer.getPlayerDeck().addToTrickOfCards(gameHand.getGameHandTable().getOutRankedCards());
					gameHand.getGameHandTable().purgeGameTable();
					continueRound = false;
					break;
				}

				System.out.println("\t\t" + currentPlayer);
				if (currentPlayer.getPlayerDeck().getDeckInHand().size() == 0) {
					System.out.println("\t\t" + currentPlayer.getPlayerName() + " is out of cards and will be skipped");
				} else {
					List<Card> playedCard = currentPlayer.playCard(isFirst, gameHand.getGameHandTable().getActiveCardSet());

					if (playedCard != null) {
						if (!isFirst) {
							System.out.println("\t\t\t" + currentPlayer.getPlayerName() + " outranked with " + playedCard);
						} else {
							System.out.println("\t\t\t" + currentPlayer.getPlayerName() + " played " + playedCard);
						}
						gameHand.getGameHandTable().outRank(playedCard, currentPlayer.getPlayerID());
					} else {
						System.out.println("\t\t\t" + currentPlayer.getPlayerName() + " passed");
					}
				}

				if (currentPlayer.getPlayerDeck().getDeckInHand().size() == 0) {
					if (gameHand.getActivePlayers().size() == 2) {
						System.out.println("\t\t" + currentPlayer.getPlayerName() + " is out of cards and only one player remains");
						continueRound = false;
					}
				}

				isFirst = false;
				if (continueRound) {
					previousPlayerIndex = currentPlayerIndex;
					currentPlayerIndex = getNextPlayerIndex(gameHand, currentPlayerIndex);
				}

			}

			// Detect players who ran out of cards
			List<ActivePlayer> outOfCardsPlayers = new ArrayList<ActivePlayer>();
			for (ActivePlayer player : gameHand.getActivePlayers()) {
				if (player.getPlayerDeck().getDeckInHand().size() == 0) {
					outOfCardsPlayers.add(player);
				}
			}

			if (outOfCardsPlayers.size() > 0) {
				for (ActivePlayer outOfCardPlayer : outOfCardsPlayers) {
					System.out.println("\t** " + outOfCardPlayer.getPlayerName() + " is out of cards");
					gameHand.getActivePlayers().remove(outOfCardPlayer);
					firePlayerOutOfCardsEvent(outOfCardPlayer.getPlayerID(), gameHand.getHandNo(), gameHand.getActivePlayers().size() + 1);
				}
			}

			String endingPlayerID = currentPlayer.getPlayerID();
			fireGameRoundEndedEvent(gameHand.getHandNo(), roundNo, endingPlayerID);
			roundWinnerID = endingPlayerID;

			int index = 0;
			for (ActivePlayer player : gameHand.getActivePlayers()) {
				if (player.getPlayerID().equals(roundWinnerID)) {
					break;
				}
				++index;
			}
			roundWinnerIndex = index;
			// roundWinnerIndex = currentPlayerIndex;
		}
	}

	/**
	 * This method will check the number of active players to determine whether to continue the hand. This is basically a criteria to continue playing
	 * the rounds. Rounds are ended when there is only one player left
	 * 
	 * @param gameHand
	 *            Instance of GameHand
	 * @return boolean Indicates whether to continue hand
	 */
	private boolean continueHand(GameHand gameHand) {
		boolean continueHand = false;
		if (gameHand.getActivePlayers().size() > 1) {
			continueHand = true;
		}
		return continueHand;
	}

	/**
	 * This method returns the array index position of the owner of the active card set
	 * 
	 * @param gameHand
	 *            Instance of game hand
	 * @return int Index position of the owner of the active card set
	 */
	private int getActiveCardSetPlayerIndex(GameHand gameHand) {
		int activeCardSetPlayerIndex = -1;

		// The owner is obtained from the game hand table
		String ownerID = gameHand.getGameHandTable().getActiveSetOwner();
		int index = 0;
		for (ActivePlayer player : gameHand.getActivePlayers()) {
			if (player.getPlayerID().equals(ownerID)) {
				activeCardSetPlayerIndex = index;
				break;
			} else {
				++index;
			}
		}

		return activeCardSetPlayerIndex;
	}

	private String getPlayerIDByIndex(GameHand gameHand, int playerIndex) {
		return gameHand.getActivePlayers().get(playerIndex).getPlayerID();
	}

	/**
	 * This method returns the array Index position of the starting player. This is determined by the dealer position. In case of first hand, the last
	 * round winner is not available and hence dealer index is use. In other hands the last winning player is the dealer and hence the player next to
	 * this player is the starting player
	 * 
	 * @param gameHand
	 *            Instance of GameHand
	 * @param lastRoundWinnerID
	 *            ID of the player who won the last round
	 * @param lastRoundWinnerIndex
	 *            Index position of the player who won the last round
	 * @return
	 */
	private int getStartingPlayerIndex(GameHand gameHand, String lastRoundWinnerID, int lastRoundWinnerIndex) {
		int startingPlayerIndex = 0;

		if (lastRoundWinnerID != null) {
			// This is a subsequent hand
			boolean playerPresent = false;
			for (ActivePlayer player : gameHand.getActivePlayers()) {
				if (player.getPlayerID().equalsIgnoreCase(lastRoundWinnerID)) {
					playerPresent = true;
					break;
				}
			}

			if (playerPresent) {
				startingPlayerIndex = lastRoundWinnerIndex;
			} else {
				startingPlayerIndex = getNextPlayerIndex(gameHand, lastRoundWinnerIndex);
			}

		} else {
			// This is the first hand
			startingPlayerIndex = (getDealerIndex(gameHand) + 1) % gameHand.getActivePlayers().size();
		}

		return startingPlayerIndex;
	}

	/**
	 * This method returns the array Index position of the next player in the active players. This is not the serial order of the player. It is a
	 * value 0-3
	 * 
	 * @param gameHand
	 *            Instance of GameHand
	 * @param currentPlayerIndex
	 *            Index position of the current player in the gam
	 * @return int Index position of the next player in the game
	 */
	private int getNextPlayerIndex(GameHand gameHand, int currentPlayerIndex) {
		int nextPlayerIndex = 0;
		nextPlayerIndex = (currentPlayerIndex + 1) % gameHand.getActivePlayers().size();
		return nextPlayerIndex;
	}

	/**
	 * This method returns the array Index of the dealer in the active players. This is not the serial order of the player. It is a value 0-3
	 * 
	 * @param gameHand
	 *            Instance of GameHand
	 * @return int array index position of the dealer
	 */
	private int getDealerIndex(GameHand gameHand) {
		int dealerIndex = 0;

		for (GamePlayer player : gameHand.getActivePlayers()) {
			if (player.getPlayerID().equals(gameHand.getDealerID())) {
				break;
			}
			++dealerIndex;
		}
		return dealerIndex;
	}

	/**
	 * This method builds a default list of GamePlayers
	 */
	private void buildGamePlayers() {
		players.add(new GamePlayer("Player_1", "PL1", true));
		players.add(new GamePlayer("Player_2", "PL2", true));
		players.add(new GamePlayer("Player_3", "PL3", true));
		players.add(new GamePlayer("Player_4", "PL4", true));
	}

	/**
	 * This method builds the player decks. It uses the DeckBuilder to shuffle and partition the card for the players based on the dealer position.
	 * This is a realistic way of dealing cards
	 * 
	 * @param builder
	 *            An instance of DeckBuilder
	 * @param dealer
	 *            GamePlayer representing the dealer
	 * @return List<PlayerDeck> list containing player deck for the four players
	 * @throws Exception
	 */
	private List<PlayerDeck> buildPlayerDecks(DeckBuilder builder, GamePlayer dealer) throws Exception {

		List<PlayerDeck> playerDecks = builder.getPlayerDecks(getDealerPosition(dealer));
		return playerDecks;
	}

	/**
	 * This method returns the position of the dealer. This is the serial position and not array Index. It can be a value 1-4
	 * 
	 * @param dealer
	 *            GamePlayer representing the dealer
	 * @return int Serial position of the dealer
	 */
	private int getDealerPosition(GamePlayer dealer) {
		int dealerPosition = 0;

		for (GamePlayer player : players) {
			++dealerPosition;
			if (player.getPlayerID().equals(dealer.getPlayerID())) {
				break;
			}
		}
		return dealerPosition;
	}

	/**
	 * This method builds a list of active players. This is used at the beginning of a hand. The ActivePlayer will be populated with PlayerDeck based
	 * on their position relative to the dealer.
	 * 
	 * @param playerDecks
	 *            List of PlayerDeck
	 * @return List<ActivePlayer> List of active players with their player decks populated.
	 */
	private List<ActivePlayer> buildActivePlayers(List<PlayerDeck> playerDecks) {
		List<ActivePlayer> activePlayers = new ArrayList<ActivePlayer>();

		int playerPosition = 0;

		for (GamePlayer player : players) {
			if (player.getIsActive()) {
				activePlayers.add(new ActivePlayer(player.getPlayerName(), player.getPlayerID(), playerDecks.get(playerPosition)));
			}
			++playerPosition;
		}
		return activePlayers;
	}

	/**
	 * This method will pick a dealer and return the GamePlayer. There are two scenarios. In the first hand, the dealer is picked randomly. For
	 * subsequent hands, the first ranking player is picked as the dealer
	 * 
	 * @param gameHand
	 *            Number of the current Hand
	 * @return GamePlayer Player who is picked at the dealer
	 */
	private GamePlayer pickDealer(GameHand gameHand) {
		GamePlayer dealer = null;
		if (gameHand.getHandNo() == 1) {
			// This is the first hand pick dealer randomly
			System.out.println("Picking dealer...");
			Random rn = new Random();
			int dealerPosition = rn.nextInt(4) + 1;
			dealer = players.get(dealerPosition - 1);
			System.out.println("\tDealer: " + dealer);
		} else {
			// Dealer is the top ranking player
			String dealerID = scoreBoard.getTopRankingPlayer();

			for (GamePlayer player : players) {

				if (player.getPlayerID().equals(dealerID)) {
					dealer = player;
					break;
				}
			}
			System.out.println(dealer + " won the previous hand and is now the dealer.");
		}
		return dealer;
	}

	/**
	 * This method get the List of active players in the game. This will be printed at beginning of the round
	 * 
	 * @param handNo
	 *            Number of the current Hand
	 * @return List<String> List of player Names
	 */
	private List<String> getActivePlayerNames(int handNo) {
		GameHand hand = gameHands.get(handNo - 1);

		List<String> playerNames = new ArrayList<String>();
		for (ActivePlayer player : hand.getActivePlayers()) {
			playerNames.add(player.getPlayerName());
		}
		return playerNames;
	}

	/**
	 * This method prints the active player decks. This will be printed at the beginning of the Hand
	 * 
	 * @param gameHand
	 *            Instance of GameHand
	 */
	private void printActivePlayerDecks(GameHand gameHand) {
		String dealerID = gameHand.getDealerID();

		for (ActivePlayer activePlayer : gameHand.getActivePlayers()) {
			if (activePlayer.getPlayerID().equals(dealerID)) {
				System.out.println("\t" + activePlayer.getPlayerName() + ":(DLR)\t" + activePlayer.getPlayerDeck());
			} else {
				System.out.println("\t" + activePlayer.getPlayerName() + ":\t" + activePlayer.getPlayerDeck());
			}

		}
	}

	/**
	 * This method prints the ScoreBoard. This will be printed at the end of the Hand
	 */
	private void printScoreBoard() {
		System.out.println("------------ Scoreboard ------------");
		for (PlayerScore playerScore : scoreBoard.getPlayerScores()) {
			System.out.println("\t" + playerScore);
		}
		System.out.println("------------------------------------");
	}

	/**
	 * This method returns a list of players with rank = 1
	 * 
	 * @return List<String> List of winners with top score
	 */
	private List<String> getTopRankingPlayers() {
		List<String> winners = new ArrayList<String>();
		for (PlayerScore playerScore : scoreBoard.getPlayerScores()) {
			if (playerScore.getPlayerPosition() == 1) {
				winners.add(playerScore.getPlayerName());
			}
		}
		return winners;
	}

	/**
	 * This method will trap the keyboard input and force user to type something
	 * 
	 * @throws Exception
	 */
	private void trapKeyBoard() throws Exception {
		Scanner scan = new Scanner(System.in);
		String text = scan.nextLine();
	}

	// EVENT TRIGGERING METHODS

	/**
	 * This method will register components implementing the GameEngineEventListener and add them to the eventlisteners list
	 * 
	 * @param listener
	 *            Component implementing the GameEngineEventListener interface
	 */
	public synchronized void addGameEngineEventListener(GameEngineEventListener listener) {
		eventlisteners.add(listener);
	}

	/**
	 * This method will deregister components implementing the GameEngineEventListener and remove them to the eventlisteners list
	 * 
	 * @param listener
	 *            Component implementing the GameEngineEventListener interface
	 */
	public synchronized void removeGameEngineEventListener(GameEngineEventListener listener) {
		eventlisteners.remove(listener);
	}

	/**
	 * This method will trigger the GameStartedEvent.
	 * 
	 * @param players
	 *            List of players in the game
	 */
	private synchronized void fireGameStartedEvent(List<GamePlayer> players) {
		GameStartedEvent event = new GameStartedEvent(this, players);
		Iterator<GameEngineEventListener> listeners = eventlisteners.iterator();
		while (listeners.hasNext()) {
			((GameEngineEventListener) listeners.next()).handleGameStarted(event);
			;
		}
		System.out.println("Started Frank's Zoo with 4 players: " + players);
	}

	/**
	 * This method will trigger the GameEndedEvent
	 */
	private synchronized void fireGameEndedEvent() {
		GameEndedEvent event = new GameEndedEvent(this);
		Iterator<GameEngineEventListener> listeners = eventlisteners.iterator();
		while (listeners.hasNext()) {
			((GameEngineEventListener) listeners.next()).handleGameEnded(event);
			;
		}
		System.out.println("Ended Frank's Zoo game!.");
		System.out.println("\tWinner(s): " + getTopRankingPlayers());
	}

	/**
	 * This method will trigger the GameHandStartedEvent
	 * 
	 * @param handNo
	 *            Number of the current Hand
	 * @param dealerID
	 *            ID of the dealer
	 */
	private synchronized void fireGameHandStartedEvent(Integer handNo, String dealerID) {
		HandStartedEvent event = new HandStartedEvent(this, handNo, dealerID);
		Iterator<GameEngineEventListener> listeners = eventlisteners.iterator();
		while (listeners.hasNext()) {
			((GameEngineEventListener) listeners.next()).handleHandStarted(event);
			;
		}
		System.out.println("Hand#: " + handNo + " Starts");

	}

	/**
	 * This method will trigger the GameHandEndedEvent
	 * 
	 * @param handNo
	 *            Number of the current Hand
	 * @param endingPlayerID
	 *            ID of the ending Player
	 * @param activePlayers
	 *            List of active players in the game
	 * @throws Exception
	 */
	private synchronized void fireGameHandEndedEvent(Integer handNo, String endingPlayerID, List<ActivePlayer> activePlayers) throws Exception {
		HandEndedEvent event = new HandEndedEvent(this, handNo, endingPlayerID);
		Iterator<GameEngineEventListener> listeners = eventlisteners.iterator();
		while (listeners.hasNext()) {
			((GameEngineEventListener) listeners.next()).handleHandEnded(event);
			;
		}

		// Determine the player Name
		String playerName = "";
		for (ActivePlayer player : activePlayers) {
			if (player.getPlayerID().equals(endingPlayerID)) {
				playerName = player.getPlayerName();
				break;
			}
		}

		System.out.println("Hand#: " + handNo + " Ended with " + playerName + " left behind\n");
		printScoreBoard();
		System.out.println("[Press Enter key to continue]");
		trapKeyBoard();
	}

	/**
	 * This method will trigger the GameRoundStartedEvent
	 * 
	 * @param handNo
	 *            Number of the current Hand
	 * @param roundNo
	 *            Number of the current Round
	 * @param startingPlayerID
	 *            ID of the player starting the game
	 * @param activePlayers
	 *            List of active players in the game
	 */
	private synchronized void fireGameRoundStartedEvent(Integer handNo, Integer roundNo, String startingPlayerID, List<ActivePlayer> activePlayers) {
		RoundStartedEvent event = new RoundStartedEvent(this, handNo, roundNo, startingPlayerID);
		Iterator<GameEngineEventListener> listeners = eventlisteners.iterator();
		while (listeners.hasNext()) {
			((GameEngineEventListener) listeners.next()).handleRoundStarted(event);
			;
		}
		// Determine the player Name
		String playerName = "";
		for (ActivePlayer player : activePlayers) {
			if (player.getPlayerID().equals(startingPlayerID)) {
				playerName = player.getPlayerName();
				break;
			}
		}
		System.out.println("\tRound#: " + roundNo + " Started by " + playerName + " with " + activePlayers.size() + " players: " + getActivePlayerNames(handNo));
	}

	/**
	 * This method will trigger the GameRoundEndedEvent
	 * 
	 * @param handNo
	 *            The number of the current Hand
	 * @param roundNo
	 *            The number of the current Round
	 * @param endingPlayerID
	 *            ID of the ending player
	 * @throws Exception
	 */
	private synchronized void fireGameRoundEndedEvent(Integer handNo, Integer roundNo, String endingPlayerID) throws Exception {
		RoundEndedEvent event = new RoundEndedEvent(this, handNo, roundNo, endingPlayerID);
		Iterator<GameEngineEventListener> listeners = eventlisteners.iterator();
		while (listeners.hasNext()) {
			((GameEngineEventListener) listeners.next()).handleRoundEnded(event);
			;
		}
		System.out.println("\tRound#: " + roundNo + " Ended");
		System.out.println("[Press Enter key to continue]");
		trapKeyBoard();
	}

	/**
	 * This method will trigger the PlayerOutOfCardsEvent
	 * 
	 * @param playerID
	 *            ID of the player
	 * @param handNo
	 *            Number of the current Hand
	 * @param activePlayerCount
	 *            Total Number of players still active
	 */
	private synchronized void firePlayerOutOfCardsEvent(String playerID, Integer handNo, Integer activePlayerCount) {
		PlayerOutOfCardsEvent event = new PlayerOutOfCardsEvent(this, playerID, handNo, activePlayerCount);
		Iterator<GameEngineEventListener> listeners = eventlisteners.iterator();
		while (listeners.hasNext()) {
			((GameEngineEventListener) listeners.next()).handlePlayerOutOfCards(event);
			;
		}
	}

}
